/**
 * @license
 * Copyright 2011 The Emscripten Authors
 * SPDX-License-Identifier: MIT
 */

import * as os from 'node:os';
import * as fs from 'node:fs';
import * as path from 'node:path';
import {fileURLToPath} from 'node:url';
import assert from 'node:assert';

import {
  debugLog,
  isDecorator,
  isJsOnlySymbol,
  error,
  readFile,
  pushCurrentFile,
  popCurrentFile,
  addToCompileTimeContext,
  runInMacroContext,
  mergeInto,
  localFile,
  timer,
} from './utility.mjs';
import {preprocess, processMacros} from './parseTools.mjs';

// Various namespace-like modules

// List of symbols that were added from the library.
export const librarySymbols = [];
// Map of library symbols which are aliases for native symbols
// e.g. `wasmTable` -> `__indirect_function_table`
export const nativeAliases = {};

const srcDir = fileURLToPath(new URL('.', import.meta.url));
const systemLibdir = path.join(srcDir, 'lib');

function isBeneath(childPath, parentPath) {
  const relativePath = path.relative(parentPath, childPath);
  return !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
}

function calculateLibraries() {
  // Core system libraries (always linked against)
  let libraries = [
    'libint53.js',
    'libcore.js',
    'libsigs.js',
    'libccall.js',
    'libaddfunction.js',
    'libgetvalue.js',
    'libmath.js',
    'libpath.js',
    'libstrings.js',
    'libhtml5.js',
    'libstack_trace.js',
    'libwasi.js',
    'libeventloop.js',
    'libpromise.js',
  ];

  if (LINK_AS_CXX) {
    if (DISABLE_EXCEPTION_THROWING && !WASM_EXCEPTIONS) {
      libraries.push('libexceptions_stub.js');
    } else {
      libraries.push('libexceptions.js');
    }
  }

  if (!MINIMAL_RUNTIME) {
    libraries.push('libbrowser.js');
    libraries.push('libwget.js');
  }

  if (!STANDALONE_WASM) {
    libraries.push('libtime.js');
  }

  if (EMSCRIPTEN_TRACING) {
    libraries.push('libmemoryprofiler.js');
  }

  if (SUPPORT_BASE64_EMBEDDING || ENVIRONMENT_MAY_BE_SHELL) {
    libraries.push('libbase64.js');
  }

  if (AUTODEBUG) {
    libraries.push('libautodebug.js');
  }

  if (!WASMFS) {
    libraries.push('libsyscall.js');
  }

  if (MAIN_MODULE || RELOCATABLE) {
    libraries.push('libdylink.js');
  }

  if (FILESYSTEM) {
    libraries.push('libfs_shared.js');
    if (WASMFS) {
      libraries.push(
        'libwasmfs.js',
        'libwasmfs_js_file.js',
        'libwasmfs_jsimpl.js',
        'libwasmfs_fetch.js',
        'libwasmfs_node.js',
        'libwasmfs_opfs.js',
      );
    } else {
      // Core filesystem libraries (always linked against, unless -sFILESYSTEM=0 is specified)
      libraries.push(
        'libfs.js',
        'libmemfs.js',
        'libtty.js',
        'libpipefs.js', // ok to include it by default since it's only used if the syscall is used
        'libsockfs.js', // ok to include it by default since it's only used if the syscall is used
      );

      if (NODERAWFS) {
        // NODERAWFS requires NODEFS
        if (!JS_LIBRARIES.includes('libnodefs.js')) {
          libraries.push('libnodefs.js');
        }
        libraries.push('libnoderawfs.js');
        // NODERAWFS overwrites libpath.js
        libraries.push('libnodepath.js');
      }
    }
  }

  // Additional JS libraries (without AUTO_JS_LIBRARIES, link to these explicitly via -lxxx.js)
  if (AUTO_JS_LIBRARIES) {
    libraries.push(
      'libwebgl.js',
      'libhtml5_webgl.js',
      'libopenal.js',
      'libglut.js',
      'libxlib.js',
      'libegl.js',
      'libuuid.js',
      'libglew.js',
      'libidbstore.js',
      'libasync.js',
    );
    if (USE_SDL != 2) {
      libraries.push('libsdl.js');
    }
  } else {
    if (ASYNCIFY) {
      libraries.push('libasync.js');
    }
    if (USE_SDL == 1) {
      libraries.push('libsdl.js');
    }
    if (USE_SDL == 2) {
      libraries.push('libegl.js', 'libwebgl.js', 'libhtml5_webgl.js');
    }
  }

  if (USE_GLFW) {
    libraries.push('libglfw.js');
  }

  if (LZ4) {
    libraries.push('liblz4.js');
  }

  if (SHARED_MEMORY) {
    libraries.push('libatomic.js');
  }

  if (MAX_WEBGL_VERSION >= 2) {
    // libwebgl2.js must be included only after libwebgl.js, so if we are
    // about to include libwebgl2.js, first squeeze in libwebgl.js.
    libraries.push('libwebgl.js');
    libraries.push('libwebgl2.js');
  }

  if (GL_EXPLICIT_UNIFORM_LOCATION || GL_EXPLICIT_UNIFORM_BINDING) {
    libraries.push('libc_preprocessor.js');
  }

  if (LEGACY_GL_EMULATION) {
    libraries.push('libglemu.js');
  }

  if (!STRICT) {
    libraries.push('liblegacy.js');
  }

  if (BOOTSTRAPPING_STRUCT_INFO) {
    libraries = ['libbootstrap.js', 'libstrings.js', 'libint53.js'];
  }

  if (SUPPORT_BIG_ENDIAN) {
    libraries.push('liblittle_endian_heap.js');
  }

  // Resolve system libraries
  libraries = libraries.map((filename) => path.join(systemLibdir, filename));

  // Add all user specified JS library files to the link.
  // These must be added last after all Emscripten-provided system libraries
  // above, so that users can override built-in JS library symbols in their
  // own code.
  libraries.push(...JS_LIBRARIES);

  // Deduplicate libraries to avoid processing any library file multiple times
  libraries = libraries.filter((item, pos) => libraries.indexOf(item) == pos);

  return libraries;
}

let tempDir;

function getTempDir() {
  if (!tempDir) {
    const tempRoot = os.tmpdir();
    tempDir = fs.mkdtempSync(path.join(tempRoot, 'emcc-jscompiler-'));
  }
  return tempDir;
}

function preprocessFiles(filenames) {
  timer.start('preprocessFiles')
  const results = {};
  for (const filename of filenames) {
    debugLog(`pre-processing JS library: ${filename}`);
    pushCurrentFile(filename);
    try {
      results[filename] = processMacros(preprocess(filename), filename);
    } catch (e) {
      error(`error preprocessing JS library "${filename}":`);
      throw e;
    } finally {
      popCurrentFile();
    }
  }
  timer.stop('preprocessFiles')
  return results;
}

export const LibraryManager = {
  library: {},
  // The JS and JS docs of each library definition indexed my mangled name.
  libraryDefinitions: {},
  structs: {},
  loaded: false,
  libraries: [],

  has(name) {
    if (!path.isAbsolute(name)) {
      // Our libraries used to be called `library_xxx.js` rather than
      // `lib_xx.js`.  In case we have external code using this function
      // we check for the old form too.
      if (name.startsWith('library_')) {
        name = name.replace('library_', 'lib');
      }
      name = path.join(systemLibdir, name);
    }
    return this.libraries.includes(name);
  },

  load() {
    timer.start('load')

    assert(!this.loaded);
    this.loaded = true;
    // Save the list for has() queries later.
    this.libraries = calculateLibraries();

    const preprocessed = preprocessFiles(this.libraries);

    timer.start('executeJS')
    for (const [filename, contents] of Object.entries(preprocessed)) {
      this.executeJSLibraryFile(filename, contents);
    }
    timer.stop('executeJS')

    timer.stop('load')
  },

  executeJSLibraryFile(filename, contents) {
    const userLibraryProxy = new Proxy(this.library, {
      set(target, prop, value) {
        target[prop] = value;
        if (!isDecorator(prop)) {
          target[prop + '__user'] = true;
        }
        return true;
      },
    });

    const isUserLibrary = !isBeneath(filename, systemLibdir);
    if (isUserLibrary) {
      debugLog(`executing user JS library: ${filename}`);
    } else {
      debugLog(`exectuing system JS library: ${filename}`);
    }

    let origLibrary;
    // When we parse user libraries also set `__user` attribute
    // on each element so that we can distinguish them later.
    if (isUserLibrary) {
      origLibrary = this.library;
      this.library = userLibraryProxy;
    }
    pushCurrentFile(filename);
    let preprocessedName = filename.replace(/\.\w+$/, '.preprocessed$&')
    if (VERBOSE) {
      preprocessedName = path.join(getTempDir(), path.basename(filename));
    }

    try {
      runInMacroContext(contents, {filename: preprocessedName})
    } catch (e) {
      error(`failure to execute JS library "${filename}":`);
      if (VERBOSE) {
        fs.writeFileSync(preprocessedName, contents);
        error(`preprocessed JS saved to ${preprocessedName}`)
      } else {
        error('use -sVERBOSE to save preprocessed JS');
      }
      throw e;
    } finally {
      popCurrentFile();
      if (origLibrary) {
        this.library = origLibrary;
      }
    }
    if (VERBOSE) {
      fs.rmSync(getTempDir(), { recursive: true, force: true });
    }
  }
};

// options is optional input object containing mergeInto params
// currently, it can contain
//
// key: noOverride, value: true
// if it is set, it prevents symbol redefinition and shows error
// in case of redefinition
//
// key: checkSig, value: true
// if it is set, __sig is checked for functions and error is reported
// if <function name>__sig is missing
function addToLibrary(obj, options = null) {
  mergeInto(LibraryManager.library, obj, options);
}

let structs = {};
let defines = {};

/**
 * Read JSON file containing struct and macro/define information
 * that can then be used in JavaScript via macros.
 */
function loadStructInfo(filename) {
  const temp = JSON.parse(readFile(filename));
  Object.assign(structs, temp.structs);
  Object.assign(defines, temp.defines);
}

if (!BOOTSTRAPPING_STRUCT_INFO) {
  // Load struct and define information.
  if (MEMORY64) {
    loadStructInfo(localFile('struct_info_generated_wasm64.json'));
  } else {
    loadStructInfo(localFile('struct_info_generated.json'));
  }
}

// Use proxy objects for C_DEFINES and C_STRUCTS so that we can give useful
// error messages.
const C_STRUCTS = new Proxy(structs, {
  get(target, prop) {
    if (!(prop in target)) {
      throw new Error(
        `Missing C struct ${prop}! If you just added it to struct_info.json, you need to run ./tools/gen_struct_info.py (then run a second time with --wasm64)`,
      );
    }
    return target[prop];
  },
});

const C_DEFINES = new Proxy(defines, {
  get(target, prop) {
    if (!(prop in target)) {
      throw new Error(
        `Missing C define ${prop}! If you just added it to struct_info.json, you need to run ./tools/gen_struct_info.py (then run a second time with --wasm64)`,
      );
    }
    return target[prop];
  },
});

// shorter alias for C_DEFINES
const cDefs = C_DEFINES;

// Legacy function that existed solely to give error message.  These are now
// provided by the cDefs proxy object above.
function cDefine(key) {
  return cDefs[key];
}

function isInternalSymbol(ident) {
  return ident + '__internal' in LibraryManager.library;
}

function getUnusedLibrarySymbols() {
  const librarySymbolSet = new Set(librarySymbols);
  const missingSyms = new Set();
  for (const [ident, value] of Object.entries(LibraryManager.library)) {
    if (typeof value === 'function' || typeof value === 'number') {
      if (isJsOnlySymbol(ident) && !isDecorator(ident) && !isInternalSymbol(ident)) {
        const name = ident.slice(1);
        if (!librarySymbolSet.has(name)) {
          missingSyms.add(name);
        }
      }
    }
  }
  return missingSyms;
}

// When running with ASSERTIONS enabled we create stubs for each library
// function that that was not included in the build.  This gives useful errors
// when library dependencies are missing from `__deps` or depended on without
// being added to DEFAULT_LIBRARY_FUNCS_TO_INCLUDE
// TODO(sbc): These errors could potentially be generated at build time via
// some kind of acorn pass that searched for uses of these missing symbols.
function addMissingLibraryStubs(unusedLibSymbols) {
  let rtn = '';
  rtn += 'var missingLibrarySymbols = [\n';
  for (const sym of unusedLibSymbols) {
    rtn += `  '${sym}',\n`;
  }
  rtn += '];\n';
  rtn += 'missingLibrarySymbols.forEach(missingLibrarySymbol)\n';
  return rtn;
}

function exportSymbol(name) {
  // In MODULARIZE=instance mode symbols are exported by being included in
  // an export { foo, bar } list so we build up the simple list of names
  if (MODULARIZE === 'instance') {
    return name;
  }
  return `Module['${name}'] = ${name};`;
}

// export parts of the JS runtime that the user asked for
function exportRuntimeSymbols() {
  // optionally export something.
  function shouldExport(name) {
    // Native exports are not available to be exported initially.  Instead,
    // they get exported later in `assignWasmExports`.
    if (nativeAliases[name]) {
      return false;
    }
    // If requested to be exported, export it.
    if (EXPORTED_RUNTIME_METHODS.has(name)) {
      // Unless we are in MODULARIZE=instance mode then HEAP objects are
      // exported separately in updateMemoryViews
      if (MODULARIZE == 'instance' || !name.startsWith('HEAP')) {
        return true;
      }
    }
    return false;
  }

  // All possible runtime elements that can be exported
  let runtimeElements = [
    'run',
    'out',
    'err',
    'callMain',
    'abort',
    'wasmExports',
    'HEAPF32',
    'HEAPF64',
    'HEAP8',
    'HEAPU8',
    'HEAP16',
    'HEAPU16',
    'HEAP32',
    'HEAPU32',
    'HEAP64',
    'HEAPU64',
  ];

  if (SUPPORT_BIG_ENDIAN) {
    runtimeElements.push('HEAP_DATA_VIEW');
  }

  if (LOAD_SOURCE_MAP) {
    runtimeElements.push('WasmSourceMap');
  }

  if (STACK_OVERFLOW_CHECK) {
    runtimeElements.push('writeStackCookie');
    runtimeElements.push('checkStackCookie');
  }

  if (RETAIN_COMPILER_SETTINGS) {
    runtimeElements.push('getCompilerSetting');
  }

  if (RUNTIME_DEBUG) {
    runtimeElements.push('prettyPrint');
  }

  // dynCall_* methods are not hardcoded here, as they
  // depend on the file being compiled. check for them
  // and add them.
  for (const name of EXPORTED_RUNTIME_METHODS) {
    if (/^dynCall_/.test(name)) {
      // a specific dynCall; add to the list
      runtimeElements.push(name);
    }
  }

  // Add JS library elements such as FS, GL, ENV, etc. These are prefixed with
  // '$ which indicates they are JS methods.
  let runtimeElementsSet = new Set(runtimeElements);
  for (const ident of Object.keys(LibraryManager.library)) {
    if (isJsOnlySymbol(ident) && !isDecorator(ident) && !isInternalSymbol(ident)) {
      const jsname = ident.slice(1);
      // Note that this assertion may be hit when a function is moved into the
      // JS library. In that case the function should be removed from the list
      // of runtime elements above.
      assert(!runtimeElementsSet.has(jsname), 'runtimeElements contains library symbol: ' + ident);
      runtimeElements.push(jsname);
    }
  }

  // check all exported things exist, error when missing
  runtimeElementsSet = new Set(runtimeElements);
  for (const name of EXPORTED_RUNTIME_METHODS) {
    if (!runtimeElementsSet.has(name)) {
      error(`undefined exported symbol: "${name}" in EXPORTED_RUNTIME_METHODS`);
    }
  }

  const exports = runtimeElements.filter(shouldExport);
  const results = exports.map(exportSymbol);

  if (MODULARIZE == 'instance') {
    if (results.length == 0) return '';
    return '// Runtime exports\nexport { ' + results.join(', ') + ' };\n';
  }

  if (ASSERTIONS && !EXPORT_ALL) {
    // in ASSERTIONS mode we show a useful error if it is used without being
    // exported.  See `unexportedRuntimeSymbol` in runtime_debug.js.
    const unusedLibSymbols = getUnusedLibrarySymbols();
    if (unusedLibSymbols.size) {
      results.push(addMissingLibraryStubs(unusedLibSymbols));
    }

    const unexported = [];
    for (const name of runtimeElements) {
      if (
        !EXPORTED_RUNTIME_METHODS.has(name) &&
        !EXPORTED_FUNCTIONS.has(name) &&
        !unusedLibSymbols.has(name)
      ) {
        unexported.push(name);
      }
    }

    if (unexported.length || unusedLibSymbols.size) {
      let unexportedStubs = 'var unexportedSymbols = [\n';
      for (const sym of unexported) {
        unexportedStubs += `  '${sym}',\n`;
      }
      unexportedStubs += '];\n';
      unexportedStubs += 'unexportedSymbols.forEach(unexportedRuntimeSymbol);\n';
      results.push(unexportedStubs);
    }
  }

  results.unshift('// Begin runtime exports');
  results.push('// End runtime exports');
  return results.join('\n  ') + '\n';
}

function exportLibrarySymbols() {
  assert(MODULARIZE != 'instance');
  const results = ['// Begin JS library exports'];
  for (const ident of librarySymbols) {
    if ((EXPORT_ALL || EXPORTED_FUNCTIONS.has(ident)) && !nativeAliases[ident]) {
      results.push(exportSymbol(ident));
    }
  }
  results.push('// End JS library exports');
  return results.join('\n  ') + '\n';
}

function exportJSSymbols() {
  // In MODULARIZE=instance mode JS library symbols are marked with `export`
  // at the point of declaration.
  if (MODULARIZE == 'instance') return exportRuntimeSymbols();
  return exportRuntimeSymbols() + '  ' + exportLibrarySymbols();
}

addToCompileTimeContext({
  exportJSSymbols,
  loadStructInfo,
  LibraryManager,
  librarySymbols,
  addToLibrary,
  cDefs,
  cDefine,
  C_STRUCTS,
  C_DEFINES,
});
